package com.spzx.product.service.impl;

import com.alibaba.nacos.common.utils.StringUtils;
import com.spzx.product.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TestServiceImpl implements TestService {

   @Autowired
   private StringRedisTemplate redisTemplate;
    
   @Override
   public synchronized void testLock() {
      // 尝试从缓存中获取业务数据，如果命中缓存则返回即可，缓存未命中，解决缓存击穿问题，先获取分布式锁
      //1.获取分布式锁
      //1.1 版本1：采用Redis提供的setnx命令  存在的问题： A线程加锁成功后，逻辑执行中服务宕机，锁无法释放造成死锁
      String lockName = "lock";
//      String lcokValue = "lock";
//      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lcokValue);

      //1.2 版本2：在版本1基础上，为锁的key设置过期时间，防止死锁    问题：加锁 和 设置锁过期时间 不具备原子性
//      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lcokValue);
      //防止死锁，通过expire命令为锁key设置过期时间
//      redisTemplate.expire(lockName,5, TimeUnit.SECONDS);

      //1.3 版本3：使用set命令配合 NX EX 参数 替换版本2的命令，确保 加锁 和 设置过期时间的原子性
//      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lcokValue, 5, TimeUnit.SECONDS);

      //1.4 版本4：在版本3基础上，存入Redis锁的值，为每个线程生成UUID作为线程标识  问题：判断锁和删除锁不是原子性操作
      String lockValue = UUID.randomUUID().toString();
      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);

      // 2.获取锁成功后，才执行业务逻辑
      if(flag){
         try {

            // 查询Redis中的num值
            String value = this.redisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)){
               return ;
            }
            // 有值就转成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
         } finally {
            // 3.释放锁
            // 4.版本4：先判断Redis中锁的值跟当前线程虚拟机栈获取当前线程标识值是否一致，一致则删除
            /*if(redisTemplate.opsForValue().get(lockName).equals(lcokValue)){
               redisTemplate.delete(lockName);
            }*/
            //1.5 版本5：使用lua脚本删除锁，确保判断锁和删除锁原子性
            //5.1 创建Redis脚本对象，封装lua脚本
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then\n" +
                             "    return redis.call('del', KEYS[1])\n" +
                             "else\n" +
                             "    return 0\n" +
                             "end";
            RedisScript<Boolean> booleanDefaultRedisScript = new DefaultRedisScript<>(script,Boolean.class);
            redisTemplate.execute(booleanDefaultRedisScript, Arrays.asList(lockName), lockValue);
         }
      }else {
         log.error("获取锁失败:{},再次尝试自旋",Thread.currentThread().getName());
         try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}
         this.testLock();
      }
   }
}